package edu.eas.gateway.authorization;

import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.nimbusds.jose.JWSObject;
import edu.eas.common.constant.AuthConstant;
import edu.eas.common.domain.UserDto;
import edu.eas.gateway.config.IgnoreUrlsConfig;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.HttpMethod;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.ReactiveAuthorizationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.web.server.authorization.AuthorizationContext;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.PathMatcher;
import reactor.core.publisher.Mono;

import java.net.URI;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

/**
 * 鉴权管理器，用于判断是否有资源的访问权限
 *
 * @author cql
 */
@Component
public class AuthorizationManager implements ReactiveAuthorizationManager<AuthorizationContext> {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    @Autowired
    private IgnoreUrlsConfig ignoreUrlsConfig;

    @Override
    public Mono<AuthorizationDecision> check(Mono<Authentication> mono, AuthorizationContext authorizationContext) {

        // 1.获取访问URL
        ServerHttpRequest request = authorizationContext.getExchange().getRequest();
        URI uri = request.getURI();

        PathMatcher pathMatcher = new AntPathMatcher();

        // 2.白名单路径直接放行
        List<String> ignoreUrls = ignoreUrlsConfig.getUrls();
        for (String ignoreUrl : ignoreUrls) {
            if (pathMatcher.match(ignoreUrl, uri.getPath())) {
                return Mono.just(new AuthorizationDecision(true));
            }
        }

        // 3.对应跨域的预检请求直接放行
        if (request.getMethod() == HttpMethod.OPTIONS) {
            return Mono.just(new AuthorizationDecision(true));
        }

        // 4.不同用户体系登录不允许互相访问
        try {
            String token = request.getHeaders().getFirst(AuthConstant.JWT_TOKEN_HEADER);
            if (StrUtil.isEmpty(token)) {
                return Mono.just(new AuthorizationDecision(false));
            }
            // TODO 不是使用RSA算法进行加密了么,为什么这里不用公钥也可以解密的?
            // 这里说的加密,指的是签名所使用的加密算法,不是数据,jwt默认是不加密的,任何人都可以读到
            String realToken = token.replace(AuthConstant.JWT_TOKEN_PREFIX, "");
            JWSObject jwsObject = JWSObject.parse(realToken);
            String userStr = jwsObject.getPayload().toString();
            UserDto userDto = JSONUtil.toBean(userStr, UserDto.class);

            // 4.1 当前是管理员身份,但是访问非管理员URL
            if (AuthConstant.USER_TYPE_ADMIN.equals(userDto.getUserType()) && !pathMatcher.match(AuthConstant.ADMIN_URL_PATTERN, uri.getPath())) {
                return Mono.just(new AuthorizationDecision(false));
            }
            // 4.2 当前是学生身份,但是访问非学生URL
            if (AuthConstant.USER_TYPE_STUDENT.equals(userDto.getUserType()) && !pathMatcher.match(AuthConstant.STUDENT_URL_PATTERN, uri.getPath())) {
                return Mono.just(new AuthorizationDecision(false));
            }
            // 4.3 当前是教师身份,但是访问非教师URL
            if (AuthConstant.USER_TYPE_TEACHER.equals(userDto.getUserType()) && !pathMatcher.match(AuthConstant.TEACHER_URL_PATTERN, uri.getPath())) {
                return Mono.just(new AuthorizationDecision(false));
            }

            // 4.4 对于内部的学生管理系统和内部的教师管理系统不需要做更细粒度的权限控制
            if (AuthConstant.TEACHER_CLIENT_ID.equals(userDto.getClientId()) || AuthConstant.STUDENT_CLIENT_ID.equals(userDto.getClientId())) {
                return Mono.just(new AuthorizationDecision(true));
            }

            // 4.5 如果是超级管理员,直接放行(id=1)
            if (AuthConstant.USER_TYPE_ADMIN.equals(userDto.getUserType()) && userDto.getId().equals(1L)) {
                return Mono.just(new AuthorizationDecision(true));
            }
        } catch (ParseException e) {
            e.printStackTrace();
            return Mono.just(new AuthorizationDecision(false));
        }

        // 5.非管理端路径直接放行(对于教师和学生角色,它们只有一个身份就是原来的身份,所有的教师和学生的权限都是一样的,无需区别;
        // 但是对于管理员来说,不同的管理员有不同的权限和访问的URL,需要要做更细粒度的权限划分
        // 改动:允许授权第三方应用开发,因此对于教师端和学生端的角色需要做更细粒度的划分
        // if (!pathMatcher.match(AuthConstant.ADMIN_URL_PATTERN, uri.getPath())) {
        //     return Mono.just(new AuthorizationDecision(true));
        // }


        // 6.管理端路径需校验权限
        Map<Object, Object> resourceRolesMap = redisTemplate.opsForHash().entries(AuthConstant.RESOURCE_ROLES_MAP_KEY);
        Iterator<Object> iterator = resourceRolesMap.keySet().iterator();
        List<String> authorities = new ArrayList<>();
        while (iterator.hasNext()) {
            String pattern = (String) iterator.next();
            if (pathMatcher.match(pattern, uri.getPath())) {
                authorities.addAll(Convert.toList(String.class, resourceRolesMap.get(pattern)));
            }
        }
        authorities = authorities.stream().map(i -> i = AuthConstant.AUTHORITY_PREFIX + i).collect(Collectors.toList());

        // 7.认证通过且角色匹配的用户可访问当前路径
        return mono
                .filter(Authentication::isAuthenticated)
                .flatMapIterable(Authentication::getAuthorities)
                .map(GrantedAuthority::getAuthority)
                .any(authorities::contains)
                .map(AuthorizationDecision::new)
                .defaultIfEmpty(new AuthorizationDecision(false));
    }
}
